{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Construct Hamiltonian \n",
    "\n",
    "*Copyright (c) 2021 Institute for Quantum Computing, Baidu Inc. All Rights Reserved.*\n",
    "\n",
    "## Outline\n",
    "\n",
    "This tutorial will demonstrate how to define a system Hamiltonian using Quanlse. The outline of this tutorial is as follows:\n",
    "\n",
    "- Introduction\n",
    "- Preparation\n",
    "- Define Hamiltonian\n",
    "- Define control pulse waveforms\n",
    "- Simulation and related tools\n",
    "- Summary"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Introduction\n",
    "\n",
    "Generally, when modeling superconducting systems, we use the Hamiltonian $\\hat{H}_{\\rm sys}$ to describe the total energy of the entire system: \n",
    "$$\n",
    "\\hat{H}_{\\rm sys} = \\hat{H}_{\\rm drift} + \\hat{H}_{\\rm coup} + \\hat{H}_{\\rm ctrl}.\n",
    "$$\n",
    "It typically contains three terms - the time-independent drift term describing the individual qubits in the system, the coupling term describing the qubits interaction, and the time-dependent control term describing the control fields.\n",
    "\n",
    "With the Hamiltonian constructed, we can simulate the evolution of the quantum system using Schrödinger equation in the Heisenberg picture and obtain the time-ordered evolution operator $U(t)$,\n",
    "$$\n",
    "i\\hbar\\frac{{\\rm d}U(t)}{{\\rm d}t} = \\hat{H}(t)U(t).\n",
    "$$\n",
    "\n",
    "In Quanlse, we can conveniently construct a Hamiltonian. We provide a variety of functions in `Utils` and a complete set of pre-defined operators, allowing the users to construct the Hamiltonian for large quantum systems with ease.\n",
    "\n",
    "## Preparation \n",
    "\n",
    "After you have successfully installed Quanlse, you can run the Quanlse program below following this tutorial. To run this particular tutorial, you would need to import the following packages from Quanlse and a constant $\\pi$ from the commonly-used Python library `math`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Import required packages\n",
    "from Quanlse.Utils import Hamiltonian as qham\n",
    "from Quanlse.Utils.Operator import *\n",
    "\n",
    "from math import pi"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Define Hamiltonian\n",
    "\n",
    "In this section, we will take a system consisting two qubits to demonstrate how to construct a Hamiltonian using Quanlse. Now, we will create such a Hamiltonian in Quanlse by individually adding the time-independent drift terms, the coupling terms and the time-dependent control terms.\n",
    "\n",
    "The first step is to define the basic information regarding the system using the function `createHam()`, which returns a Python dictionary. Its arguments include the sampling-time `dt` (in nanoseconds), number of qubits `qubitNum`, and the system's energy level `sysLevel`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ham = qham.createHam(title='hamiltonian', dt=1, qubitNum=2, sysLevel=3)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In Quanlse 1.1.0, the function `createHam()` allows the users to define different energy levels for qubits in the same system. The users can pass the qubits' energy levels as a list to `sysLevel`. For instance, when `qubitNum=2`, we can pass `[2, 10]` to `sysLevel` to define a system with dimension $\\mathcal{H}^{2} \\otimes \\mathcal{H}^{10}$ ($\\mathcal{H}^n$ indicates the $n$-dimensional Hilbert space).\n",
    "\n",
    "Then, we add the drift Hamiltonian, which includes the detuning and the anharmonicity terms:\n",
    "$$\n",
    "\\hat{H}_{\\rm drift} = \\sum_{i=0}^1(\\omega_i-\\omega_d)\\hat{a}_i^\\dagger\\hat{a}_i + \\sum_{i=0}^1 \\frac{\\alpha_i}{2} \\hat{a}_i^{\\dagger}\\hat{a}_i^{\\dagger}\\hat{a}_i\\hat{a}_i.\n",
    "$$ \n",
    "\n",
    "Here, the first term on the right side of the equation represents the detuning of the qubit and the driving field, and the second term represents the anharmonicity of the qubit, which is the inherent characteristic of superconducting qubits. These terms are added into `ham` by the function `addDrift()`. We pass in the according parameters and the matrix forms of the operators which are pre-defined in the module `Operator`. ([Clik here](https://quanlse.baidu.com/api/Utils/Utils.Operator.html) to view all pre-defined operators)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# The qubit frequency, GHz\n",
    "wq = [4.887 * (2 * pi), 4.562 * (2 * pi)]  \n",
    "\n",
    "# Qubits anharmonicity, GHz\n",
    "anharm = [- 0.317 * (2 * pi), - 0.317 * (2 * pi)]\n",
    "\n",
    "# The drive pulse is in resonance  \n",
    "wd = wq[0]\n",
    "\n",
    "# Add drift terms to the Hamiltonian\n",
    "for q in range(2):\n",
    "    qham.addDrift(ham, name=f'detuning{q}', onQubits=q, matrices=number(3), amp=wq[q] - wd)\n",
    "    qham.addDrift(ham, name=f'anharm{q}', onQubits=q, matrices=duff(3), amp=anharm[q] / 2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Since there are two qubits in this system, we can use a `for` loop of two `addDrift()` functions to add the drift terms. The first parameter is the Python dictionary created by `createHam()`; `name` is a user-defined identifier of this term; `onQubits` indicates which qubit(s) the term corresponds to; `matrices` takes the matrix representation of the term; and `amp` is the coefficient before this term.\n",
    "\n",
    "Next, let us add the coupling terms which describe the interaction between qubits. Here, the coupling term can be written:\n",
    "$$\n",
    "\\hat{H}_{\\rm coup} = \\frac{g_{01}}{2} (\\hat{a}_0\\hat{a}_1^\\dagger + \\hat{a}_0^\\dagger\\hat{a}_1).\n",
    "$$ \n",
    "\n",
    "In Quanlse, only one line of code is needed to add the coupling term - we use the function `addCoupling()` and select the qubits' indices and specify a coupling strength. Note that the argument `g` is the coefficient before the term, thus includes the $\\frac{1}{2}$ from the definition above."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Add the coupling terms\n",
    "qham.addCoupling(ham, name='coupling', onQubits=[0, 1], g=0.0277 * (2 * pi) / 2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Lastly, we add the control terms. In a superconducting system, the control Hamiltonian represents external control (microwave, magnetic flux, etc.) applied onto the qubits. To apply X-Y control onto qubit 0, the corresponding control terms are described by:\n",
    "$$\n",
    "\\hat{H}_{\\rm ctrl} = A_0^x(t)\\frac{\\hat{a}_0+\\hat{a}_0^\\dagger}{2} + A_0^y(t) i\\frac{\\hat{a}_0-\\hat{a}_0^\\dagger}{2}. \n",
    "$$ \n",
    "\n",
    "We use the following code to add the control terms above,"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Add the control terms\n",
    "qham.addControl(ham, name='q0-ctrlx', onQubits=0, matrices=driveX(3))\n",
    "qham.addControl(ham, name='q0-ctrly', onQubits=0, matrices=driveY(3))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Here, the `name` parameter is a unique user-defined identifier.\n",
    "Note: this argument will be used when we define a particular waveform of a selected control term.\n",
    "\n",
    "## Define control pulse waveforms\n",
    "\n",
    "After we define the system Hamiltonian, we can then add specific waveforms to the control terms and implement specific quantum operations. In Quanlse, users can use functions `setwave()` and `addwave()` to define waveforms. They are used in a similar fashion, except that `setwave()` clears all the existing waveforms in the specified control term, and `addwave()` does not. Additionally, users can use `clearWave()` to clear all waveforms in the specified control term.\n",
    "\n",
    "Here, we will take `setWave()` as the example to define the control waveforms. Each waveform function $A(t)$ can be defined using four arguments: start time `t0`, duration `t`, the function of the waveform `f`, and the corresponding parameters `para`. The function `setWave()` allows us to define waveforms in three different ways:\n",
    "\n",
    "- **using preset waveform functions:**\n",
    "\n",
    "Users can input the name `string` of the waveform to call preset functions. The supported waveforms are listed here: [API](https://quanlse.baidu.com/api/_modules/Quanlse/Utils/Waveforms.html#play).\n",
    "```python\n",
    "p = {\"a\": 1.1, \"tau\": 10, \"sigma\": 8}\n",
    "qham.setWave(ham, \"q0-ctrlx\", t0=0, t=20, f=\"gaussian\", para=p)\n",
    "```\n",
    "\n",
    "- **using user-defined wave functions:**\n",
    "\n",
    "Users can also define a function in the form of `func(t, args)`, where the first parameter `t` is the time duration and `args` is the pulse parameters from the argument `para` of the function `setWave()`.\n",
    "```python\n",
    "def UserWaveFunc(t, args):\n",
    "    return args[\"a\"] + args[\"b\"] + args[\"c\"]\n",
    "p = {\"a\": 1.1, \"b\": 5.3, \"c\": 3.2}\n",
    "qham.setWave(ham, \"q0-ctrlx\", t0=0, t=20, f=UserWaveFunc, para=p)\n",
    "```\n",
    " \n",
    "- **using user-defined wave sequence:**\n",
    "\n",
    "Users can directly input a list of pulse amplitudes to `seq`. In the following code, `s` is a user-defined Python list.\n",
    "```python\n",
    "qham.setWave(ham, \"q0-ctrlx\", t0=0, seq=s)\n",
    "```\n",
    "\n",
    "In this example, we add the pre-defined Gaussian waveforms to the X and Y control terms:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "qham.setWave(ham, \"q0-ctrlx\", t0=0, t=20, f=\"gaussian\", para={\"a\": 1.1, \"tau\": 10, \"sigma\": 4})\n",
    "qham.setWave(ham, \"q0-ctrly\", t0=0, t=20, f=\"drag_y1\", para={\"a\": 1.7, \"tau\": 10, \"sigma\": 4})"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Waveform construction in Quanlse also supports alternative related functions. As an example, we can use the function  `makeWaveData()` ([see here](https://quanlse.baidu.com/api/Utils/Utils.Waveforms.html#Quanlse.Utils.Waveforms.makeWaveData)) to define a Python dictionary of the waveform, which is quite convenient when packaging. At the same time, users can also use the function `addWaveData()` ([see here](https://quanlse.baidu.com/api/Utils/Utils.Hamiltonian.html#Quanlse.Utils.Hamiltonian.addWaveData)) to define waveforms in batches.\n",
    "\n",
    "## Simulation and related tools\n",
    "\n",
    "With the Hamiltonian and control pulses defined, we can use the function `simulate()` to calculate the time-ordered evolution operator:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "qham.simulate(ham)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can also display the detailed information regarding the constructed Hamiltonian using the function `printHam()`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "qham.printHam(ham)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The pulse sequence for each control term can be extracted by `getPulseSequences()`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(qham.getPulseSequences(ham, names='q0-ctrly'))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "To visualize the pulse sequence, use `plotWaves()`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "qham.plotWaves(ham, ['q0-ctrlx', 'q0-ctrly'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Summary\n",
    "\n",
    "The purpose of this tutorial is to introduce how to construct, simulate and visualize Hamiltonian using Quanlse. After reading this tutorial, users can use this link [tutorial-construct-hamiltonian.ipynb](https://github.com/baidu/Quanlse/tree/master/Tutorial/EN/tutorial-construct-hamiltonian.ipynb) to jump to the GitHub page corresponding to this Jupyter Notebook and download the code above. Users are encouraged to try different parameter values or functions to gain a better understanding."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python [conda env:Quanlse]",
   "language": "python",
   "name": "quanlse"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.8"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}